Skip to content

Render colored inline diffs for file-edit tool calls#83

Merged
TroyHernandez merged 4 commits into
mainfrom
diff-color
May 14, 2026
Merged

Render colored inline diffs for file-edit tool calls#83
TroyHernandez merged 4 commits into
mainfrom
diff-color

Conversation

@TroyHernandez
Copy link
Copy Markdown
Contributor

Summary

When the agent ran replace_in_file or write_file the user saw just ⎿ 1 line in 12ms and had no idea what actually changed. Now the display shows a Claude-Code-style inline diff: an Added N, removed M summary followed by colored unified-diff hunks (red minus, green plus, cyan hunk header, dim metadata).

Implementation

  • R/diff-render.R::compute_unified_diff(old, new, path) shells out to diff -u. Returns NULL when inputs are identical (display silently skips), an all-green payload for new files, and a fallback size summary when diff isn't on PATH so the tool call still succeeds.
  • ok_with_diff(text, diff) is a thin builder; ok() itself is untouched so the diff extension doesn't bleed into the package's general tool-result contract (per codex's earlier review).
  • One unified path for both tools. tool_replace_in_file passes its existing original/updated strings — no special substring math; tool_write_file reads prior content before the write and diffs against the post-write content (handling append mode).
  • Display layer. turn.R threads the diff payload through events; observer_progress() and the CLI tool_handler both render via render_tool_diff() when present.
  • colorize_diff() in R/cli-colors.R is the shared line painter; also wired into the /diff slash command so its output is colored too.
  • LLM-facing result text is unchanged. The model already knows what it asked for; the diff is purely for the human.

Test plan

  • 27 new tinytests in test_diff_render.R. Full suite: 1590/1590 pass.
  • corteza::chat() → ask the agent to edit a file → confirm inline colored diff appears.
  • ~/bin/corteza same.
  • /diff slash command now colored.
  • Tool result text the LLM sees is still Updated <path> (1 replacement) / Wrote N bytes to <path>.

When the agent ran replace_in_file or write_file the user saw just
"⎿ 1 line in 12ms" and had no idea what actually changed. Now the
display shows a Claude-Code-style inline diff: an "Added N, removed M"
summary followed by colored unified-diff hunks (red minus, green plus,
cyan hunk header, dim metadata), so the change is visible at a glance.

Implementation:

- New R/diff-render.R::compute_unified_diff(old, new, path) shells out
  to `diff -u`. Returns NULL when the inputs are identical (display
  silently skips), an all-green payload for new files (old == ""), and
  a fallback summary when `diff` isn't on PATH so the tool call still
  succeeds.
- New ok_with_diff(text, diff) in R/utils.R is a thin builder; ok()
  itself is untouched so the diff-extension doesn't bleed into the
  package's general tool-result contract.
- tool_replace_in_file passes its existing `original`/`updated`
  strings; tool_write_file reads prior content before the write and
  diffs against the post-write content (taking append mode into
  account).
- turn.R threads the diff payload through outcome_text -> event;
  observer_progress() renders via render_tool_diff() when present.
- inst/bin/corteza tool_handler does the same render.
- colorize_diff() in R/cli-colors.R is the shared line painter; also
  wired into the /diff slash command so its output is colored too.
- LLM-facing result text is unchanged. The model already knows what
  it asked for; the diff is purely for the human reading the terminal.

27 new tinytests cover identical / new file / emptied / multi-hunk /
missing trailing newline / `diff`-not-on-PATH fallback.
Codex flagged that compute_unified_diff() was unbounded: a 1000-line
new file dumped 1003 lines into chat and CLI scrollback (and shipped
the same volume across the callr worker boundary). Cap the lines
vector with a "[diff truncated: N more lines]" marker so the user can
see the size of the change without drowning in it. Summary counts
still reflect the full diff so "Added 1000 lines" stays accurate.

The new max_lines / max_chars params default to 200 / 20000 — tighter
than the 300 / 60000 used by /diff because inline tool diffs live in
chat scrollback rather than a one-shot command output. Both budgets
are tunable; pass Inf to disable.
@TroyHernandez
Copy link
Copy Markdown
Contributor Author

Addressed in d0bf67c: compute_unified_diff() now caps lines at 200 / 20000 chars with a [diff truncated: N more lines] marker (defaults tunable via max_lines / max_chars). summary still reflects the full diff. Verified end-to-end: a 1000-line new file via tool_write_file() returns 201 lines + marker instead of 1003, while the summary still reads Added 1000 lines. Added two truncation tests (line budget, char budget) to test_diff_render.R; full suite still 1590/1590.

…numbers

Codex flagged that the CLI script and corteza::chat() were making
separate decisions about color support and inline-diff layout, which
is exactly how the "CLI is colored but chat() isn't" drift just hit
the user. Centralize the shared bits before the gap widens.

Color policy is now sourced from R/cli-colors.R only:

- ansi_supported() learns NO_COLOR / FORCE_COLOR / RSTUDIO so RStudio's
  R console (which is not a tty) and override env vars both work.
- inst/bin/corteza deletes its private .ansi_supported() and inline
  16-entry color list; both surfaces call corteza:::ansi_colors().

Inline diffs rendered through the same renderer everywhere:

- render_tool_diff() now drops the redundant `--- /path` and `+++ /path`
  lines (the path is already in the tool-call title) and the `@@` hunk
  headers. It parses each hunk header for starting line numbers, walks
  the body, and emits one row per line as `NNNN +|-| content` with red
  on removals, green on additions, default on context. Truncation
  marker passes through dim.
- Tool labels: replace_in_file -> "Update", write_file -> "Write" to
  match Claude Code phrasing and match the rendered diff context.

This is the scoped step Codex suggested; a full console_ui() module
that owns every cat() in the CLI is a follow-up, filed separately.
@TroyHernandez
Copy link
Copy Markdown
Contributor Author

Addressed codex's drift concern in fc9788a — scoped centralization:

  • ansi_supported() / ansi_colors() source-of-truth in R/cli-colors.R. NO_COLOR / FORCE_COLOR / RSTUDIO all there. CLI script deletes its private copies.
  • render_tool_diff() now strips --- /path, +++ /path, and @@ hunk markers (path is already in the tool-call title; line numbers replace the hunk header). One row per kept line as NNNN +|-| content with red/green color.
  • Tool labels: replace_in_file → "Update", write_file → "Write" so the title matches Claude Code phrasing.

Full console_ui() module migration (every CLI cat() routed through one helper) tracked as #84.

@TroyHernandez TroyHernandez merged commit f0a0689 into main May 14, 2026
4 checks passed
@TroyHernandez TroyHernandez deleted the diff-color branch May 14, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant